// Use of this source code is governed by a BSD-style license
// that can be found in the License file.
//
// Author: Shuo Chen (chenshuo at chenshuo dot com)
#include "netlib/base/time_zone.h"

#include <endian.h>

#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <string>
#include <vector>

#include "netlib/base/date.h"
#include "netlib/base/noncopyable.h"

namespace netlib {
namespace detail {

struct Transition {
	time_t gmttime_;
	time_t localtime_;
	int localtime_idx_;

	Transition(time_t t, time_t l, int localIdx) :
	    gmttime_(t), localtime_(l), localtime_idx_(localIdx) {}
};

struct Comp {
	bool compare_gmt_;

	explicit Comp(bool gmt) : compare_gmt_(gmt) {}

	bool operator()(const Transition& lhs, const Transition& rhs) const {
		if (compare_gmt_) {
			return lhs.gmttime_ < rhs.gmttime_;
		}
		return lhs.localtime_ < rhs.localtime_;
	}

	bool Equal(const Transition& lhs, const Transition& rhs) const {
		if (compare_gmt_) {
			return lhs.gmttime_ == rhs.gmttime_;
		}
		return lhs.localtime_ == rhs.localtime_;
	}
};

struct Localtime {
	time_t gmt_offset_;
	bool is_dst_;
	int arrb_idx_;

	Localtime(time_t offset, bool dst, int arrb) :
	    gmt_offset_(offset), is_dst_(dst), arrb_idx_(arrb) {}
};

inline void FillHms(unsigned seconds, struct tm* utc) {
	utc->tm_sec = seconds % 60;
	unsigned minutes = seconds / 60;
	utc->tm_min = minutes % 60;
	utc->tm_hour = minutes / 60;
}

} // namespace detail
const int kSecondsPerDay = 24 * 60 * 60;
} // namespace netlib

using namespace netlib;
using namespace std;

struct TimeZone::Data {
	vector<detail::Transition> transitions_;
	vector<detail::Localtime> localtimes_;
	vector<string> names_;
	string abbreviation_;
};

namespace netlib::detail {

class File : NonCopyable {
public:
	explicit File(const char* file) : fp_(::fopen(file, "rb")) {}

	~File() {
		if (fp_) {
			::fclose(fp_);
		}
	}

	bool Valid() const { return fp_; }

	string ReadBytes(int n) {
		char buf[n];
		ssize_t nr = ::fread(buf, 1, n, fp_);
		if (nr != n) {
			throw logic_error("no enough data");
		}
		// return string(buf, n);
		return {buf, static_cast<std::size_t>(n)};
	}

	int32_t ReadInt32() {
		int32_t x = 0;
		ssize_t nr = ::fread(&x, 1, sizeof(int32_t), fp_);
		if (nr != sizeof(int32_t)) {
			throw logic_error("bad int32_t data");
		}
		return be32toh(x);
	}

	uint8_t ReadUInt8() {
		uint8_t x = 0;
		ssize_t nr = ::fread(&x, 1, sizeof(uint8_t), fp_);
		if (nr != sizeof(uint8_t)) {
			throw logic_error("bad uint8_t data");
		}
		return x;
	}

private:
	FILE* fp_;
};

bool ReadTimeZoneFile(const char* zonefile, struct TimeZone::Data* data) {
	File f(zonefile);
	if (f.Valid()) {
		try {
			string head = f.ReadBytes(4);
			if (head != "TZif") {
				throw logic_error("bad head");
			}
			string version = f.ReadBytes(1);
			f.ReadBytes(15);

			int32_t isgmtcnt = f.ReadInt32();
			int32_t isstdcnt = f.ReadInt32();
			int32_t leapcnt = f.ReadInt32();
			int32_t timecnt = f.ReadInt32();
			int32_t typecnt = f.ReadInt32();
			int32_t charcnt = f.ReadInt32();

			vector<int32_t> trans;
			vector<int> localtimes;
			trans.reserve(static_cast<std::size_t>(timecnt));
			for (int i = 0; i < timecnt; ++i) {
				trans.push_back(f.ReadInt32());
			}

			for (int i = 0; i < timecnt; ++i) {
				uint8_t local = f.ReadUInt8();
				localtimes.push_back(local);
			}

			for (int i = 0; i < typecnt; ++i) {
				int32_t gmtoff = f.ReadInt32();
				uint8_t isdst = f.ReadUInt8();
				uint8_t abbrind = f.ReadUInt8();

				data->localtimes_.emplace_back(gmtoff, isdst, abbrind);
			}

			for (int i = 0; i < timecnt; ++i) {
				int local_idx = localtimes[i];
				time_t localtime = trans[i] + data->localtimes_[local_idx].gmt_offset_;
				data->transitions_.emplace_back(trans[i], localtime, local_idx);
			}

			data->abbreviation_ = f.ReadBytes(charcnt);

			// leapcnt
			for (int i = 0; i < leapcnt; ++i) {
				// int32_t leaptime = f.readInt32();
				// int32_t cumleap = f.readInt32();
			}
			// FIXME
			(void)isstdcnt;
			(void)isgmtcnt;
		} catch (logic_error& e) {
			fprintf(stderr, "%s\n", e.what());
		}
	}
	return true;
}

const Localtime* FindLocaltime(const TimeZone::Data& data, Transition sentry, Comp comp) {
	const Localtime* local = nullptr;

	if (data.transitions_.empty() || comp(sentry, data.transitions_.front())) {
		// FIXME: should be first non dst time zone
		local = &data.localtimes_.front();
	} else {
		auto trans_i =
		    lower_bound(data.transitions_.begin(), data.transitions_.end(), sentry, comp);
		if (trans_i != data.transitions_.end()) {
			if (!comp.Equal(sentry, *trans_i)) {
				assert(trans_i != data.transitions_.begin());
				--trans_i;
			}
			local = &data.localtimes_[trans_i->localtime_idx_];
		} else {
			// FIXME: use TZ-env
			local = &data.localtimes_[data.transitions_.back().localtime_idx_];
		}
	}

	return local;
}

} // namespace netlib::detail

TimeZone::TimeZone(const char* zonefile) : data_(new TimeZone::Data) {
	if (!detail::ReadTimeZoneFile(zonefile, data_.get())) {
		data_.reset();
	}
}

TimeZone::TimeZone(int eastOfUtc, const char* name) : data_(new TimeZone::Data) {
	data_->localtimes_.emplace_back(eastOfUtc, false, 0);
	data_->abbreviation_ = name;
}

struct tm TimeZone::ToLocalTime(time_t seconds) const {
	struct tm local_time;
	memset(&local_time, '\0', sizeof(local_time));
	assert(data_ != nullptr);
	const Data& data(*data_);

	detail::Transition sentry(seconds, 0, 0);
	const detail::Localtime* local = FindLocaltime(data, sentry, detail::Comp(true));

	if (local) {
		time_t local_seconds = seconds + local->gmt_offset_;
		::gmtime_r(&local_seconds, &local_time); // FIXME: fromUtcTime
		local_time.tm_isdst = local->is_dst_;
		local_time.tm_gmtoff = local->gmt_offset_;
		local_time.tm_zone = &data.abbreviation_[local->arrb_idx_];
	}

	return local_time;
}

time_t TimeZone::FromLocalTime(const struct tm& localTm) const {
	assert(data_ != nullptr);
	const Data& data(*data_);

	struct tm tmp = localTm;
	time_t seconds = ::timegm(&tmp); // FIXME: toUtcTime
	detail::Transition sentry(0, seconds, 0);
	const detail::Localtime* local = FindLocaltime(data, sentry, detail::Comp(false));
	if (localTm.tm_isdst) {
		struct tm try_tm = ToLocalTime(seconds - local->gmt_offset_);
		if (!try_tm.tm_isdst && try_tm.tm_hour == localTm.tm_hour &&
		    try_tm.tm_min == localTm.tm_min) {
			// FIXME: HACK
			seconds -= 3600;
		}
	}
	return seconds - local->gmt_offset_;
}

struct tm TimeZone::ToUtcTime(time_t secondsSinceEpoch, bool yday) {
	struct tm utc;
	memset(&utc, '\0', sizeof(utc));
	utc.tm_zone = "GMT";
	int seconds = static_cast<int>(secondsSinceEpoch % kSecondsPerDay);
	int days = static_cast<int>(secondsSinceEpoch / kSecondsPerDay);
	if (seconds < 0) {
		seconds += kSecondsPerDay;
		--days;
	}
	detail::FillHms(seconds, &utc);
	Date date(days + Date::kJulianDayOf19700101);
	Date::YearMonthDay ymd = date.GetYearMonthDay();
	utc.tm_year = ymd.year_ - 1900;
	utc.tm_mon = ymd.month_ - 1;
	utc.tm_mday = ymd.day_;
	utc.tm_wday = date.WeekDay();

	if (yday) {
		Date start_of_year(ymd.year_, 1, 1);
		utc.tm_yday = date.JulianDayNumber() - start_of_year.JulianDayNumber();
	}
	return utc;
}

time_t TimeZone::FromUtcTime(const struct tm& utc) {
	return FromUtcTime(utc.tm_year + 1900, utc.tm_mon + 1, utc.tm_mday, utc.tm_hour, utc.tm_min,
	                   utc.tm_sec);
}

time_t TimeZone::FromUtcTime(int year, int month, int day, int hour, int minute, int seconds) {
	Date date(year, month, day);
	int seconds_in_day = hour * 3600 + minute * 60 + seconds;
	time_t days = date.JulianDayNumber() - Date::kJulianDayOf19700101;
	return days * kSecondsPerDay + seconds_in_day;
}
